core: Use libgpgme to add GPG signatures to detached metadata for commit object
authorJeremy Whiting <jeremy.whiting@collabora.com>
Tue, 3 Sep 2013 01:43:49 +0000 (19:43 -0600)
committerColin Walters <walters@verbum.org>
Sat, 28 Sep 2013 20:12:35 +0000 (16:12 -0400)
Add an optional dependency on gpgme to add GPG signatures into the
detached metadata, with the key "ostree.gpgsigs", as an "aay", an
array of signatures (treated as binary data).

The commit command gains a --gpg-sign=<key-id> argument.  Also add an
argument --gpg-homedir to set the GPG homedir where we look for
keyrings.

13 files changed:
Makefile-libostree.am
Makefile-tests.am
configure.ac
src/libostree/ostree-repo.c
src/libostree/ostree-repo.h
src/libotutil/ot-variant-utils.c
src/libotutil/ot-variant-utils.h
src/ostree/ot-builtin-commit.c
tests/gpghome/pubring.gpg [new file with mode: 0644]
tests/gpghome/secring.gpg [new file with mode: 0644]
tests/gpghome/trustdb.gpg [new file with mode: 0644]
tests/libtest.sh
tests/test-gpg-signed-commit.sh [new file with mode: 0644]

index 7925a26d70649ce8fe1d2905a8bd4e4b28d91350..badbb92def2ae64790d782a8d06cab9a2c6cfbc3 100644 (file)
@@ -99,3 +99,8 @@ CLEANFILES += $(gir_DATA) $(typelib_DATA)
 endif
 
 pkgconfig_DATA += src/libostree/ostree-1.pc
+
+if USE_GPGME
+libostree_1_la_LIBADD += $(GPGME_LIBS)
+endif
+
index 09ca24d0c82e15349160061d894ffea30d0d4570..d6b3e44842b08a6e76484982b2088f784b2d4931 100644 (file)
@@ -28,6 +28,7 @@ testfiles = test-basic \
        test-pull-archive-z \
        test-pull-corruption \
        test-pull-resume \
+       test-gpg-signed-commit \
        test-admin-deploy-1 \
        test-admin-deploy-2 \
        test-admin-deploy-uboot \
@@ -41,6 +42,11 @@ insttest_DATA = tests/archive-test.sh \
        tests/libtest.sh \
        $(NULL)
 
+gpginsttestdir = $(pkglibexecdir)/installed-tests/gpghome
+gpginsttest_DATA = tests/gpghome/secring.gpg \
+       tests/gpghome/pubring.gpg \
+       tests/gpghome/trustdb.gpg
+
 %.test: tests/%.sh Makefile
        $(AM_V_GEN) (echo '[Test]' > $@.tmp; \
         echo 'Exec=$(pkglibexecdir)/installed-tests/$(notdir $<)' >> $@.tmp; \
index d28987615aa3fc87c034ebd238587aa6cb7a12d1..20bc7d9a92a32f81a02d2e3300199f6b7181f84c 100644 (file)
@@ -81,6 +81,31 @@ m4_ifdef([GOBJECT_INTROSPECTION_CHECK], [
 ])
 AM_CONDITIONAL(BUILDOPT_INTROSPECTION, test x$found_introspection = xyes)
 
+LIBGPGME_DEPENDENCY="1.1.8"
+
+AC_ARG_WITH(gpgme,
+            AS_HELP_STRING([--without-gpgme], [Do not use gpgme]),
+            :, with_gpgme=maybe)
+
+AS_IF([ test x$with_gpgme != xno ], [
+   AC_MSG_CHECKING([for $LIBGPGME_DEPENDENCY])
+   m4_ifdef([AM_PATH_GPGME], [
+      AM_PATH_GPGME($LIBGPGME_DEPENDENCY, have_gpgme=yes, have_gpgme=no)
+      ],[
+      AM_CONDITIONAL([have_gpgme],[false])
+      ])
+   AC_MSG_RESULT([$have_gpgme])
+   AS_IF([ test x$have_gpgme = xno && test x$with_gpgme != xmaybe ], [
+       AC_MSG_ERROR([gpgme is enabled but could not be found])
+   ])
+   AS_IF([ test x$have_gpgme = xyes], [
+       AC_DEFINE(HAVE_GPGME, 1, [Define if we have gpgme])
+       with_gpgme=yes
+       ], [ with_gpgme=no ])
+], [ with_gpgme=no ])
+if test x$with_gpgme != xno; then OSTREE_FEATURES="$OSTREE_FEATURES +gpgme"; fi
+AM_CONDITIONAL(USE_GPGME, test $with_gpgme != no)
+
 LIBARCHIVE_DEPENDENCY="libarchive >= 2.8.0"
 
 GTK_DOC_CHECK([1.15], [--flavour no-tmpl])
@@ -154,6 +179,7 @@ echo "
     introspection:                                $found_introspection
     libsoup (retrieve remote HTTP repositories):  $with_soup
     libarchive (parse tar files directly):        $with_libarchive
+    gpgme (sign commits):                         $with_gpgme
     documentation:                                $enable_gtk_doc
     gjs-based tests:                              $have_gjs
     dracut:                                       $with_dracut"
index 28ec7ff6509f5eb042691dc13ad04cf78ee5fb68..189740f01d1c1efecf3374b1d9cd3a3401da20cf 100644 (file)
@@ -24,6 +24,7 @@
 
 #include <glib-unix.h>
 #include <gio/gunixinputstream.h>
+#include <gio/gfiledescriptorbased.h>
 #include "otutil.h"
 #include "libgsystem.h"
 
 #include "ostree-repo-private.h"
 #include "ostree-repo-file.h"
 
+#ifdef HAVE_GPGME
+#include <locale.h>
+#include <gpgme.h>
+#include <glib/gstdio.h>
+#endif
+
 /**
  * SECTION:libostree-repo
  * @title: Content-addressed object store
@@ -1463,3 +1470,180 @@ ostree_repo_pull (OstreeRepo               *self,
   return FALSE;
 }
 #endif
+
+#ifdef HAVE_GPGME
+gboolean
+ostree_repo_sign_commit (OstreeRepo     *self,
+                         const gchar    *commit_checksum,
+                         const gchar    *key_id,
+                         const gchar    *homedir,
+                         GCancellable   *cancellable,
+                         GError        **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_object GFile *commit_path = NULL;
+  gs_unref_variant GVariant *metadata = NULL;
+  gs_free gchar *commit_filename = NULL;
+  gs_unref_object GFile *tmp_signature_file = NULL;
+  gs_unref_object GOutputStream *tmp_signature_output = NULL;
+  gs_unref_variant_builder GVariantBuilder *builder = NULL;
+  gs_unref_variant_builder GVariantBuilder *signature_builder = NULL;
+  gs_unref_variant GVariant *commit_variant = NULL;
+  gs_unref_variant GVariant *signaturedata = NULL;
+  gs_unref_bytes GBytes *signature_bytes = NULL;
+  gpgme_ctx_t context;
+  gpgme_engine_info_t info;
+  gpgme_error_t err;
+  gpgme_key_t key = NULL;
+  gpgme_data_t commit_buffer = NULL;
+  gpgme_data_t signature_buffer = NULL;
+  int signature_fd = -1;
+  gpgme_sign_result_t result;
+  GMappedFile *signature_file = NULL;
+  
+  if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_COMMIT,
+                                 commit_checksum, &commit_variant, error))
+    goto out;
+  
+  if (!ostree_repo_read_commit_detached_metadata (self,
+                                                  commit_checksum,
+                                                  &metadata,
+                                                  cancellable,
+                                                  error))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Unable to read existing detached metadata");
+      goto out;
+    }
+
+  if (!gs_file_open_in_tmpdir (self->tmp_dir, 0644,
+                               &tmp_signature_file, &tmp_signature_output,
+                               cancellable, error))
+    goto out;
+
+  gpgme_check_version (NULL);
+  gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL));
+  
+  if ((err = gpgme_new (&context)) != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Unable to create gpg context");
+      goto out;
+    }
+
+  info = gpgme_ctx_get_engine_info (context);
+  
+  if (homedir != NULL)
+    {
+      if ((err = gpgme_ctx_set_engine_info (context, info->protocol, info->file_name, homedir))
+          != GPG_ERR_NO_ERROR)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Unable to set gpg homedir");
+          goto out;
+        }
+    }
+
+  /* Get the secret keys with the given key id */
+  if ((err = gpgme_get_key (context, key_id, &key, 1)) != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No gpg key found with the given key-id");
+      goto out;
+    }
+  
+  /* Add the key to the context as a signer */
+  if ((err = gpgme_signers_add (context, key)) != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Error signing commit");
+      goto out;
+    }
+  
+  if ((err = gpgme_data_new_from_mem (&commit_buffer, g_variant_get_data (commit_variant),
+                                      g_variant_get_size (commit_variant), FALSE)) != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Failed to create buffer from commit file");
+      goto out;
+    }
+  
+  signature_fd = g_file_descriptor_based_get_fd ((GFileDescriptorBased*)tmp_signature_output);
+  if (signature_fd < 0)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Unable to open signature file");
+      goto out;
+    }
+  
+  if ((err = gpgme_data_new_from_fd (&signature_buffer, signature_fd)) != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Failed to create buffer for signature file");
+      goto out;
+    }
+  
+  if ((err = gpgme_op_sign (context, commit_buffer, signature_buffer, GPGME_SIG_MODE_DETACH))
+      != GPG_ERR_NO_ERROR)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Failure signing commit file");
+      goto out;
+    }
+  
+  result = gpgme_op_sign_result (context);
+
+  if (!g_output_stream_close (tmp_signature_output, cancellable, error))
+    goto out;
+  
+  signature_file = gs_file_map_noatime (tmp_signature_file, cancellable, error);
+  if (!signature_file)
+    goto out;
+  signature_bytes = g_mapped_file_get_bytes (signature_file);
+  
+  // Now read the file and put its contents into the result GVariant
+  if (metadata)
+    {
+      builder = ot_util_variant_builder_from_variant (metadata, G_VARIANT_TYPE ("a{sv}"));
+      signaturedata = g_variant_lookup_value (metadata, "ostree.gpgsigs", G_VARIANT_TYPE ("aay"));
+      if (signaturedata)
+        signature_builder = ot_util_variant_builder_from_variant (signaturedata, G_VARIANT_TYPE ("aay"));
+    }
+  if (!builder)
+    builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
+  if (!signature_builder)
+    signature_builder = g_variant_builder_new (G_VARIANT_TYPE ("aay"));
+
+  g_variant_builder_add (signature_builder, "@ay", ot_gvariant_new_ay_bytes (signature_bytes));
+
+  g_variant_builder_add (builder, "{sv}", "ostree.gpgsigs", g_variant_builder_end (signature_builder));
+  
+  metadata = g_variant_builder_end (builder);
+
+  if (!ostree_repo_write_commit_detached_metadata (self,
+                                                   commit_checksum,
+                                                   metadata,
+                                                   cancellable,
+                                                   error))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Unable to read existing detached metadata");
+      goto out;
+    }
+
+  ret = TRUE;
+out:
+  if (commit_buffer)
+    gpgme_data_release (commit_buffer);
+  if (signature_buffer)
+    gpgme_data_release (signature_buffer);
+  if (key)
+    gpgme_key_release (key);
+  if (context)
+    gpgme_release (context);
+  if (signature_file)
+    g_mapped_file_unref (signature_file);
+  return ret;
+}
+
+#endif
index d55874f84cc05b4bf38c08d44637c81f5a231d55..4f40b9f9a4cd9a66b5e395f9a9b960bc2a0e7bca 100644 (file)
@@ -22,6 +22,7 @@
 
 #pragma once
 
+#include "config.h"
 #include "ostree-core.h"
 #include "ostree-types.h"
 
@@ -462,5 +463,14 @@ gboolean ostree_repo_pull (OstreeRepo             *self,
                            GCancellable           *cancellable,
                            GError                **error);
 
+#ifdef HAVE_GPGME
+gboolean ostree_repo_sign_commit (OstreeRepo     *self,
+                                  const gchar    *commit_checksum,
+                                  const gchar    *key_id,
+                                  const gchar    *homedir,
+                                  GCancellable   *cancellable,
+                                  GError        **error);
+#endif
+
 G_END_DECLS
 
index 417975f651a5510cea8d83b3a7e97e85c4c2fa23..291f7466482ad4eb3c42ff1647e2df33d207b8fc 100644 (file)
@@ -199,3 +199,23 @@ ot_variant_read (GVariant             *variant)
   return (GInputStream*)ret;
 }
 
+GVariantBuilder *
+ot_util_variant_builder_from_variant (GVariant            *variant,
+                                      const GVariantType  *type)
+{
+  GVariantBuilder *builder = NULL;
+  gint i, n;
+  
+  builder = g_variant_builder_new (type);
+  
+  n = g_variant_n_children (variant);
+  for (i = 0; i < n; i++)
+    {
+      GVariant *child = g_variant_get_child_value (variant, i);
+      g_variant_builder_add_value (builder, child);
+      g_variant_unref (child);
+    }
+    
+  return builder;
+}
+
index 83a3f54096281549db802c2a4e57d7ede991f724..92746a2d259f3135e7d873e241ce07e945d17bb4 100644 (file)
@@ -55,5 +55,8 @@ gboolean ot_util_variant_from_stream (GInputStream         *src,
 
 GInputStream *ot_variant_read (GVariant             *variant);
 
+GVariantBuilder *ot_util_variant_builder_from_variant (GVariant            *variant,
+                                                       const GVariantType  *type);
+
 G_END_DECLS
 
index 2cbe22f15c0758b58916992cc6360104470db511..e9c030d7827500246a8306326e665384769c6ab4 100644 (file)
@@ -41,6 +41,10 @@ static char **opt_trees;
 static gint opt_owner_uid = -1;
 static gint opt_owner_gid = -1;
 static gboolean opt_table_output;
+#ifdef HAVE_GPGME
+static char **opt_key_ids;
+static char *opt_gpg_homedir;
+#endif
 
 static GOptionEntry options[] = {
   { "subject", 's', 0, G_OPTION_ARG_STRING, &opt_subject, "One line subject", "subject" },
@@ -57,6 +61,10 @@ static GOptionEntry options[] = {
   { "skip-if-unchanged", 0, 0, G_OPTION_ARG_NONE, &opt_skip_if_unchanged, "If the contents are unchanged from previous commit, do nothing", NULL },
   { "statoverride", 0, 0, G_OPTION_ARG_FILENAME, &opt_statoverride_file, "File containing list of modifications to make to permissions", "path" },
   { "table-output", 0, 0, G_OPTION_ARG_NONE, &opt_table_output, "Output more information in a KEY: VALUE format", NULL },
+#ifdef HAVE_GPGME
+  { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "GPG Key ID to sign the commit with", "key-id"},
+  { "gpg-homedir", 0, 0, G_OPTION_ARG_STRING, &opt_gpg_homedir, "GPG Homedir to use when looking for keyrings", "homedir"},
+#endif
   { NULL }
 };
 
@@ -462,6 +470,26 @@ ostree_builtin_commit (int argc, char **argv, OstreeRepo *repo, GCancellable *ca
             goto out;
         }
 
+#ifdef HAVE_GPGME
+      if (opt_key_ids)
+        {
+          char **iter;
+
+          for (iter = opt_key_ids; iter && *iter; iter++)
+            {
+              const char *keyid = *iter;
+
+              if (!ostree_repo_sign_commit (repo,
+                                            commit_checksum,
+                                            keyid,
+                                            opt_gpg_homedir,
+                                            cancellable,
+                                            error))
+                goto out;
+            }
+        }
+#endif
+
       ostree_repo_transaction_set_ref (repo, NULL, opt_branch, commit_checksum);
 
       if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error))
diff --git a/tests/gpghome/pubring.gpg b/tests/gpghome/pubring.gpg
new file mode 100644 (file)
index 0000000..502a1a3
Binary files /dev/null and b/tests/gpghome/pubring.gpg differ
diff --git a/tests/gpghome/secring.gpg b/tests/gpghome/secring.gpg
new file mode 100644 (file)
index 0000000..635e20c
Binary files /dev/null and b/tests/gpghome/secring.gpg differ
diff --git a/tests/gpghome/trustdb.gpg b/tests/gpghome/trustdb.gpg
new file mode 100644 (file)
index 0000000..aeb46cb
Binary files /dev/null and b/tests/gpghome/trustdb.gpg differ
index c421b45253fb8a5230e5ac113fb88cab0d2d6117..84fd88f505251bb233b31a34943207909bc11cd3 100644 (file)
@@ -22,6 +22,9 @@ test_tmpdir=$(pwd)
 
 export G_DEBUG=fatal-warnings
 
+export TEST_GPG_KEYID="472CDAFA"
+export TEST_GPG_HOME=${SRCDIR}/gpghome
+
 if test -n "${OT_TESTS_DEBUG}"; then
     set -x
 fi
diff --git a/tests/test-gpg-signed-commit.sh b/tests/test-gpg-signed-commit.sh
new file mode 100644 (file)
index 0000000..1166f86
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/bash
+#
+# Copyright (C) 2013 Jeremy Whiting <jeremy.whiting@collabora.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -e
+
+if ! ostree --version | grep -q -e '\+gpgme'; then
+    exit 77
+fi
+
+. $(dirname $0)/libtest.sh
+
+setup_test_repository "archive-z2"
+
+cd ${test_tmpdir}
+${OSTREE} commit -b test2 -s "A GPG signed commit" -m "Signed commit body" --gpg-sign=${TEST_GPG_KEYID} --gpg-homedir=${TEST_GPG_HOME} --tree=dir=files
+$OSTREE show --print-detached-metadata-key=ostree.gpgsigs test2 > test2-gpgsigs
+# We at least got some content here and ran through the code; later
+# tests will actually do verification
+assert_file_has_content test2-gpgsigs 'byte '
+
+# Now sign a commit 3 times (with the same key)
+cd ${test_tmpdir}
+${OSTREE} commit -b test2 -s "A GPG signed commit" -m "Signed commit body" --gpg-sign=${TEST_GPG_KEYID} --gpg-sign=${TEST_GPG_KEYID} --gpg-sign=${TEST_GPG_KEYID} --gpg-homedir=${TEST_GPG_HOME} --tree=dir=files
+$OSTREE show --print-detached-metadata-key=ostree.gpgsigs test2 > test2-gpgsigs
+assert_file_has_content test2-gpgsigs 'byte '